﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.IO;
using System.Security;
using System.Security.AccessControl;
using Microsoft.Data.Schema.SchemaModel;
using System.Reflection;
using System.Diagnostics;
using Microsoft.VisualStudio.TextTemplating;
using System.Runtime.Remoting;
using Microsoft.VisualStudio.Data.Schema.Package.Project;

namespace TFDP.Common.T4
{
    public sealed class GenericTemplateProcessor : IDisposable
    {
        #region Members
        private string templateDirectory;
        private string projectDirectory;
        private string assemblyBasePath;
        private string currentConfiguration;
        private string projectId;
        private TemplateHost templateHost;
        private Engine templateEngine;

        private CSharpCodeProvider provider;
        private CompilerParameters compilerParameters;

        private Dictionary<string, object> instances;

        private const string assemblyNamespace = "TFDP.Common.T4.GeneratedTemplates";
        #endregion

        #region Properties
        /// <summary>
        /// The directory in the current project where the templates are stored
        /// </summary>
        public string TemplateDirectory
        {
            get { return templateDirectory; }
            set { templateDirectory = value; }
        }

        public string ProjectDirectory
        {
            get { return projectDirectory; }
            set
            {
                projectDirectory = value;
                InitializeAssemblyPath();
            }
        }

        public string CurrentConfiguration
        {
            get { return currentConfiguration; }
            set
            {
                currentConfiguration = value;
                InitializeAssemblyPath();
            }
        }

        /// <summary>
        /// The path to the directory where the generated assemblies are stored
        /// </summary>
        public string AssemblyBasePath
        {
            get { return assemblyBasePath; }
            private set { assemblyBasePath = value; }
        }

        public string ProjectId
        {
            get { return projectId; }
            set { projectId = value; }
        }

        public bool IsTemplateDirectorySet
        {
            get { return !String.IsNullOrEmpty(templateDirectory); }
        }

        private TemplateHost Host
        {
            get { return this.templateHost; }
        }

        private Engine Engine
        {
            get { return this.templateEngine; }
        }
        #endregion

        #region Constructor
        public GenericTemplateProcessor()
        {
            templateHost = new TemplateHost();
            templateEngine = new Engine();
            instances = new Dictionary<string, object>();

            InitializeAssemblyPath();

            // Initialize Assembly resolving
            AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
        }

        private void InitializeAssemblyPath()
        {
            if (String.IsNullOrEmpty(ProjectDirectory) || String.IsNullOrEmpty(CurrentConfiguration))
            {
                AssemblyBasePath = Path.GetTempPath();
                return;
            }

            // Obtain the AssemblyStore path and create it if it doesn't exist.
            string path = Path.Combine(ProjectDirectory, "obj", CurrentConfiguration, "AssemblyStore");
            if (!Directory.Exists(path))
                Directory.CreateDirectory(path);

            AssemblyBasePath = path;
        }
        #endregion

        /// <summary>
        /// Resolve the path to the given template file based on 
        /// the directory specified in the TemplateDirectory property
        /// </summary>
        /// <param name="templateFile">The template file to resolve</param>
        /// <returns>The resolved path to the template file</returns>
        private string ResolveTemplateFile(string templateFile)
        {
            return Path.Combine(TemplateDirectory, templateFile);
        }

        /// <summary>
        /// Generate the output file based on the given template file and element.
        /// </summary>
        /// <typeparam name="T">The type for which the template is executed. T must implement IModelElement</typeparam>
        /// <param name="templateFile">The filename of the template</param>
        /// <param name="element">The source element</param>
        /// <returns>The generated output of the template or String.Empty if a failure occurs</returns>
        public string GenerateWithTemplate(string templateFile, DataSchemaModel model, out IEnumerable<GeneratedArtifact> artifacts)
        {
            if (IsDisposed)
                throw new ObjectDisposedException("TemplateProcessor");

            artifacts = new List<GeneratedArtifact>();

            string templatePath = ResolveTemplateFile(templateFile);

            // Check if templatePath exists
            if (!File.Exists(templatePath))
            {
                Logger.LogWarning("Template " + templateFile + " skipped because it does not exist");

                return String.Empty;
            }

            Host.TemplateFile = templatePath;

            Template template = null;

            // Prepare the template
            try
            {
                template = PrepareTemplate(templatePath);
            }
            catch (Exception ex)
            {
                Logger.LogExtended(
                            TraceLevel.Error,
                            "Template preparation failed: " + ex.Message,
                            templatePath,
                            1,
                            1
                        );

                Logger.LogVerbose(ex.ToString());

                return String.Empty;
            }

            // Execute the template
            try
            {
                this.Host.SetSchemaModel(model);
                this.Host.Initialize();

                template.Host = this.Host;

                template.Initialize();

                artifacts = this.Host.Artifacts;

                return template.TransformText();
            }
            catch (GenerationCanceledException)
            {
                return String.Empty;
            }
            catch (Exception ex)
            {
                Exception actual = ex;

                if (ex is TargetInvocationException && ex.InnerException != null)
                {
                    actual = ex.InnerException;
                }

                if (actual is GenerationCanceledException)
                {
                    Logger.LogExtended(
                        TraceLevel.Info,
                        String.Format("Template transformation aborted"),
                        templatePath,
                        0,
                        0
                    );
                }
                else
                {
                    var targetSite = actual.TargetSite != null ? actual.TargetSite.Name : String.Empty;

                    Logger.LogExtended(
                        TraceLevel.Error,
                        String.Format("Template transformation failed: {0} {1}", actual.Message, targetSite),
                        templatePath,
                        1,
                        1
                    );

                    Logger.LogVerbose(actual.ToString());
                }
                return String.Empty;
            }
        }

        /// <summary>
        /// Initialize the code provider so that templates can be prepared.
        /// </summary>
        /// <param name="references">A collection of assembly references</param>
        /// <param name="outputFile">The path to the assembly that will be generated</param>
        private void InitializeCodeProvider(string[] references, string outputFile)
        {
            List<string> assemblies = new List<string>();

            assemblies.Add("System.dll");
            Assembly asm = Assembly.Load("Microsoft.VisualStudio.TextTemplating.10.0, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
            assemblies.Add(asm.Location);

            foreach (string reference in references)
            {
                try
                {
                    asm = Assembly.LoadWithPartialName(reference);
                    if (asm == null)
                        continue;
                    assemblies.Add(asm.Location);
                }
                catch (Exception)
                {
                    continue;
                }
            }

            this.compilerParameters = new CompilerParameters(
                    assemblies.ToArray()
            )
            {
                GenerateInMemory = false,
                GenerateExecutable = false,
                OutputAssembly = outputFile,
                IncludeDebugInformation = true
            };

            this.provider = new CSharpCodeProvider();
        }

        private Template PrepareTemplate(string templateFile)
        {
            // Check if the AssemblyBasePath exists
            if (!Directory.Exists(AssemblyBasePath))
                Directory.CreateDirectory(AssemblyBasePath);

            // Set up basic settings
            string className = ClassNameFromTemplateFile(templateFile);
            string assemblyPath = Path.Combine(AssemblyBasePath, className + ".dll");
            string fullName = String.Format("{0}.{1}", assemblyNamespace, className);
            Assembly assembly = null;
            Type templateType = null;
            bool assemblyRegenerated = false;
            dynamic template = null;

            // Create the FileInfo instance and refresh it
            // since this gets cached
            FileInfo fi = new FileInfo(templateFile);
            fi.Refresh();

            // Check if the assembly is older than the templatefile.
            // This is true if the template has been modified.
            if (File.GetLastWriteTime(assemblyPath) < fi.LastWriteTime)
            {
                // Signal that we're recompiling the template
                RaiseTemplateRecompile(templateFile);

                // Read the contents of the templatefile
                string templateContents = File.ReadAllText(templateFile);
                string preProcessedTemplateFile = Path.GetTempFileName();
                string language = String.Empty;
                string[] references = null;

                // Pre-process the template
                string result = this.Engine.PreprocessTemplate(
                    templateContents,
                    this.Host,
                    className,
                    assemblyNamespace,
                    out language,
                    out references
                );

                // Write the output of the preprocessed template 
                // to the temporary file
                File.WriteAllText(preProcessedTemplateFile, result);

                // Obtain a temporary path for the assembly output
                string assemblyTempPath = Path.Combine(AssemblyBasePath, String.Format("{0}.dll", className));
                //string assemblyTempPath = String.Format("{0}.{1}.dll", Path.GetTempFileName(), className);

                InitializeCodeProvider(references, assemblyTempPath);

                // Compile the pre-processed template to an assembly
                CompilerResults res = provider.CompileAssemblyFromFile(
                    compilerParameters,
                    new string[] { preProcessedTemplateFile }
                );

                // Check for compile errors
                if (res.Errors.HasErrors)
                {
                    foreach (CompilerError error in res.Errors)
                        Logger.LogExtended(
                            TraceLevel.Error,
                            error.ErrorText,
                            templateFile,
                            error.Line,
                            error.Column
                        );

                    //// Remove the temporary assembly
                    //File.Delete(assemblyTempPath);

                    throw new Exception("Compile Errors in template " + preProcessedTemplateFile);
                }

                // Remove the code file
                File.Delete(preProcessedTemplateFile);

                // Move the output assembly to the correct
                // location if necessary
                if (res.PathToAssembly != assemblyPath)
                {
                    if (File.Exists(assemblyPath))
                        File.Delete(assemblyPath);

                    File.Copy(res.PathToAssembly, assemblyPath); // File.Move
                    //File.Delete(res.PathToAssembly);
                }

                assemblyRegenerated = true;
            }

            //// Check if the assembly is already loaded
            //if (!assemblyRegenerated)
            //{
            //    if (instances.ContainsKey(className))
            //        template = instances[className];
            //    else
            //    {
            //        ObjectHandle handle = Activator.CreateInstance(className, fullName);
            //        template = handle.Unwrap();

            //        // Store the instance for future use
            //        instances.Add(className, template);
            //    }
            //}
            //else
            //{
                // Load assembly from bytes instead of file
                // to prevent file locking on the assembly
                byte[] buff = File.ReadAllBytes(assemblyPath);
                assembly = Assembly.Load(buff);

                // Obtain the type from the assembly
                templateType = assembly.GetType(fullName);

                if (templateType == null)
                    throw new Exception("Failed to load class " + fullName);

                // Create the instance
                template = Activator.CreateInstance(templateType);

            //    // Store the instance
            //    if (instances.ContainsKey(className))
            //        instances[className] = template;
            //    else
            //        instances.Add(className, template);
            //}

            return new Template(template);
        }

        /// <summary>
        /// Convert a templatefilename such as Test.tt to a classname
        /// usable by the templateprocessor.
        /// </summary>
        /// <param name="templateFile">The filename of the template</param>
        /// <returns>A valid classname</returns>
        private string ClassNameFromTemplateFile(string templateFile)
        {
            string className = Path.GetFileNameWithoutExtension(templateFile);

            foreach (char c in Path.GetInvalidFileNameChars())
                if (className.Contains(c))
                    className = className.Replace(c, (char)0x0);

            return className;
        }

        /// <summary>
        /// Resolve assemblies when it can't be found in the defined probe paths
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        /// <returns></returns>
        Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        {
            // Check the current AppDomain for the requested assembly.
            // For IGenerators and such it's the most likely location
            // they can be found.
            Assembly[] loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();

            foreach (Assembly a in loadedAssemblies)
                // Match FullName or name + , for the first part of the assembly name
                if (a.FullName == args.Name || a.FullName.Contains(args.Name + ","))
                    return a;

            string tryPath = Path.Combine(AssemblyBasePath, String.Format("{0}.dll", args.Name));
            if (File.Exists(tryPath))
            {
                byte[] buff = File.ReadAllBytes(tryPath);

                return Assembly.Load(buff);
            }

            // Probe the PublicAssemblies directory of Visual Studio
            string visualStudioPubAssemblyPath = @"C:\Program Files\Microsoft Visual Studio 10.0\Common7\IDE\PublicAssemblies";
            tryPath = Path.Combine(visualStudioPubAssemblyPath, String.Format("{0}.dll", args.Name));
            if (File.Exists(tryPath))
            {
                byte[] buff = File.ReadAllBytes(tryPath);

                return Assembly.Load(buff);
            }

            return null;
        }

        public event EventHandler<TemplateRecompileEventArgs> TemplateRecompile;

        private void RaiseTemplateRecompile(string templateFile)
        {
            if (TemplateRecompile != null)
            {
                TemplateRecompile(this, new TemplateRecompileEventArgs() { TemplateFile = templateFile });
            }
        }

        private bool isDisposed;
        private object disposeLock = new object();

        public bool IsDisposed
        {
            get
            {
                lock (disposeLock)
                {
                    return isDisposed;
                }
            }
        }

        public void Dispose()
        {
            if (!IsDisposed)
            {
                lock (disposeLock)
                {
                    isDisposed = true;
                }

                Dispose(true);
            }
        }

        private void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (provider != null)
                {
                    provider.Dispose();
                    provider = null;
                }
                if (templateHost != null)
                {
                    templateHost = null;
                }
                if (templateEngine != null)
                {
                    templateEngine = null;
                }
                if (compilerParameters != null)
                {
                    compilerParameters = null;
                }
                if (instances != null)
                {
                    instances = null;
                }
            }

            GC.SuppressFinalize(this);
        }
    }

    internal class Template
    {
        private Type templateType;
        private object instance;
        private Func<string> transformation;
        private Func<ITextTemplatingEngineHost> hostGetter;
        private Action<ITextTemplatingEngineHost> hostSetter;
        private Action initializer;

        public Template(object instance)
        {
            if (instance is DatabaseTemplate<IModelElement>)
                return;

            this.instance = instance;
            this.templateType = instance.GetType();

            MethodInfo miTransformText = this.templateType.GetMethod("TransformText", BindingFlags.Instance | BindingFlags.Public);

            transformation = () =>
            {
                object output = miTransformText.Invoke(this.instance, null);
                if (output == null)
                {
                    return String.Empty;
                }
                else
                {
                    return (string)output;
                }
            };

            MethodInfo miInitialize = this.templateType.GetMethod("Initialize", BindingFlags.Instance | BindingFlags.Public);

            if (miInitialize != null)
            {
                initializer = () =>
                {
                    miInitialize.Invoke(this.instance, null);
                };
            }

            PropertyInfo piHost = this.templateType.GetProperty("Host", BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty | BindingFlags.GetProperty);
            PropertyInfo piTemplatingHost = this.templateType.GetProperty("TemplatingHost", BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty | BindingFlags.GetProperty);
            if (piHost != null)
            {
                hostGetter = () =>
                {
                    return (ITextTemplatingEngineHost)(piHost.GetGetMethod()).Invoke(this.instance, null);
                };

                hostSetter = (v) =>
                {
                    piHost.GetSetMethod().Invoke(this.instance, new object[] { v });
                    if (piTemplatingHost != null)
                        piTemplatingHost.GetSetMethod().Invoke(this.instance, new object[] { v });
                };
            }
        }

        public string TransformText()
        {
            return transformation();
        }

        public ITextTemplatingEngineHost Host
        {
            get
            {
                if (hostGetter != null)
                {
                    return hostGetter();
                }
                else
                {
                    return null;
                }
            }
            set
            {
                if (hostSetter != null)
                {
                    hostSetter(value);
                }
            }
        }

        public void Initialize()
        {
            if (this.initializer != null)
            {
                this.initializer();
            }
        }
    }
}
